Skip to content

feat(conversation): budget exhaustion becomes a pause with invisible resume - #14

Merged
thatswiftdev merged 1 commit into
mainfrom
wt/no-hassle-budget
Oct 21, 2026
Merged

thatswiftdev merged 1 commit into
mainfrom
wt/no-hassle-budget

Conversation

@thatswiftdev

Copy link
Copy Markdown
Contributor

Budget exhaustion becomes a pause with invisible resume

Lionidas plan plans/20261021T100758Z-hop-budget-exhausted-sess-e7cb501-plan.md Stage A. Forensics: Lionidas session sess_e7cb501 died with hop budget exhausted (12) — 12 model calls (~240k tokens) and 445 chunks of genuine work reduced to one error line, and the session flipped to error with no way to continue except restarting from scratch (which re-burns the same hops and dies again).

What changes

  • Graceful exhaustion (transport/conversation.ts catch seam): LoopError with code === 'BUDGET_EXCEEDED' now stashes the full partial transcript — LoopError.partial, tool results included — via new Storage.stashPartial/takePartial (new butler_turn_partials table, one row per session, sqlite + postgres impls). The session stays active, and the turn fails with "ran out of runway — the work so far is kept; send a message to continue from where it stopped."
  • Invisible resume (send seam): the next message in that session seeds the stashed transcript ahead of history, plus a [resume] system note ("continue from where it stopped — do not repeat completed steps"). One-shot: the stash is consumed on take. A corrupt stash can never kill the next turn.
  • Everything else keeps today's behavior: ABORTED and non-budget failures still poison the session to error — only the budget path softens. The wall-clock guard (provider-level abort at the deadline) and hop guard are untouched.

Tests (test/conversation-budget.test.ts, 4/4)

Stash roundtrip (one-shot + replace-wins) · exhaust → friendly fail + active + the next provider call OBSERVES the seeded tool-role messages (history rows never carry role tool — this proves resume) · per-session isolation (no cross-session stash leakage) · ABORTED still poisons (regression guard).

Gates: tsc 0, full suite 40 files / 230 passed, 5 skipped (single fork).

Consumer note (Lionidas Stage B rides separately): Storage gained two required members — consumers implementing their own storage must add them; Lionidas uses butler's own SqliteStorage, so no action there.

…resume

no-hassle long-running turns — Lionidas plan 20261021T100758Z Stage A
(forensics: sess_e7cb501 died at hop 12 with 445 chunks of real work
reduced to one error line, session poisoned to error).

- catch seam: LoopError BUDGET_EXCEEDED stashes the full partial
  transcript (LoopError.partial — tool results included) via new
  Storage.stashPartial/takePartial (butler_turn_partials, one row per
  session, sqlite + postgres), keeps the session ACTIVE, and fails the
  turn with 'ran out of runway — the work so far is kept; send a
  message to continue from where it stopped'. ABORTED and every other
  failure keep today's honest session poisoning.
- send seam: the next message in that session seeds the stashed
  transcript ahead of history plus a [resume] system note ('continue
  from where it stopped — do not repeat completed steps'); the stash is
  consumed one-shot (takePartial deletes). A corrupt stash can never
  kill the next turn.
- tests: stash roundtrip one-shot+replace; exhaust → friendly fail +
  active + seeded tool-role messages observed by the next provider
  call; per-session isolation (no cross-session leakage); ABORTED
  still poisons (regression guard).

Gates: tsc 0, budget spec 4/4, full suite 40 files / 230 passed
5 skipped (single fork).
@thatswiftdev
thatswiftdev merged commit fcf75f8 into main Oct 21, 2026
1 check passed
@thatswiftdev
thatswiftdev deleted the wt/no-hassle-budget branch October 21, 2026 10:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant